# PooledHiLoSequence.java

    package org.codefilarete.stalactite.mapping.id.sequence.hilo;

    import org.codefilarete.stalactite.engine.SeparateTransactionExecutor;
    import org.codefilarete.stalactite.mapping.id.sequence.hilo.PooledHiLoSequencePersister.Sequence;
    import org.codefilarete.stalactite.sql.Dialect;

    /**
     * Long identifier generator for an entity class with pooling system.
     * This class reserves a range of identifiers (see {@link PooledHiLoSequenceOptions#getPoolSize()} and consumes them in
     * memory. Each time its pool is empty it goes back to the database to ask for another set of identifiers. Though if
     * JVM is shutdown while pool is not totally consumed, then a bunch of identifiers are definitively lost for the system.
     *
     * It stores the state of its sequence in a table (which can be shared between sequences, see {@link PooledHiLoSequencePersister}).
     * Stored state is the highest value reserved by current instance. Then external systems may use upper values without
     * constraint but to write their own highest reserved value.
     *
     * Inspired by "enhanced table generator" with Hilo Optimizer from Hibernate.
     *
     * @author Guillaume Mary
     */
    public class PooledHiLoSequence implements org.codefilarete.tool.function.Sequence<Long> {

    	private LongPool sequenceState;

    	private final PooledHiLoSequencePersister persister;

    	private final PooledHiLoSequenceOptions options;

    	public PooledHiLoSequence(PooledHiLoSequenceOptions options, Dialect dialect, SeparateTransactionExecutor separateTransactionExecutor, int jdbcBatchSize) {
    		this(options, new PooledHiLoSequencePersister(options.getStorageOptions(), dialect, separateTransactionExecutor, jdbcBatchSize));
    	}

    	public PooledHiLoSequence(PooledHiLoSequenceOptions options, PooledHiLoSequencePersister persister) {
    		this.options = options;
    		this.persister = persister;
    	}

    	public PooledHiLoSequencePersister getPersister() {
    		return persister;
    	}

    	public PooledHiLoSequenceOptions getOptions() {
    		return options;
    	}

    	/**
    	 * Synchronized because multiple Thread may access this instance to insert their entities.
    	 *
    	 * @return never null
    	 */
    	@Override
    	public synchronized Long next() {
    		if (sequenceState == null) {
    			// No state yet so we create one
    			initSequenceState();
    		}
    		return sequenceState.nextValue();
    	}

    	private void initSequenceState() {
    		String sequenceName = this.options.getSequenceName();
    		Sequence existingSequence = this.persister.select(sequenceName);
    		long initialValue = existingSequence == null ? this.options.getInitialValue() : existingSequence.getStep();
    		// we decrement initialValue to compensate for LongPool incrementing first value on its next() call
    		this.sequenceState = new LongPool(options.getPoolSize(), --initialValue) {
    			@Override
    			void onBoundReached() {
    				persister.reservePool(sequenceName, getPoolSize());
    			}
    		};
    		// we consider that a new boundary is reached in order to insert next state
    		sequenceState.onBoundReached();
    	}

    	/**
    	 * Range of long. Used as a pool for incrementable long values.
    	 * Notifies when upper bound is reached.
    	 * @see #onBoundReached()
    	 */
    	private abstract static class LongPool {
    		/** Pool size. Doesn't change */
    		private final int poolSize;
    		/** Incremented value */
    		private long currentValue;
    		/** Upper bound for current range. Is incremented by poolSize when reached by currentValue */
    		protected long upperBound;

    		public LongPool(int poolSize, long currentValue) {
    			this.poolSize = poolSize;
    			this.currentValue = currentValue;
    			nextBound();
    		}

    		public int getPoolSize() {
    			return poolSize;
    		}

    		public long getCurrentValue() {
    			return currentValue;
    		}

    		/**
    		 * Returns nextValue: currentValue + 1.
    		 * Calls {@link #onBoundReached()} if the upperBound is reached.
    		 *
    		 * @return currentValue + 1
    		 */
    		public long nextValue() {
    			if (currentValue == upperBound) {
    				onBoundReached();
    				nextBound();
    			}
    			currentValue++;
    			return currentValue;
    		}

    		/**
    		 * Changes upper bound: currentValue + poolSize
    		 */
    		protected void nextBound() {
    			this.upperBound = currentValue + poolSize;
    		}

    		/**
    		 * Called when upper bound is reached
    		 */
    		abstract void onBoundReached();
    	}
    }